package pl.edu.agh.agents.gui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.BoxLayout;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JToolBar;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.border.BevelBorder;

import pl.edu.agh.agents.agent.Agent;
import pl.edu.agh.agents.agent.AgentStrategy;
import pl.edu.agh.agents.agent.AgentTypes;
import pl.edu.agh.agents.agent.ControlledAgent;
import pl.edu.agh.agents.agent.SystemManager;
import pl.edu.agh.agents.gui.actions.FitAction;
import pl.edu.agh.agents.gui.actions.GetColorAction;
import pl.edu.agh.agents.gui.actions.IterationAction;
import pl.edu.agh.agents.gui.actions.IterationThread;
import pl.edu.agh.agents.gui.actions.IterationThreadAction;
import pl.edu.agh.agents.gui.actions.LoadAction;
import pl.edu.agh.agents.gui.actions.NewAction;
import pl.edu.agh.agents.gui.actions.RemoveAction;
import pl.edu.agh.agents.gui.actions.ResetAction;
import pl.edu.agh.agents.gui.actions.SaveAction;
import pl.edu.agh.agents.gui.actions.ShowGenerateDialogAction;
import pl.edu.agh.agents.gui.actions.ZoomAction;
import pl.edu.agh.agents.planner.CentralPlanner;
import pl.edu.agh.agents.state.State;
import pl.edu.agh.agents.state.StateSpace;

import com.mxgraph.model.mxCell;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.view.mxGraph;

public class GUI extends JFrame {
	private static final long serialVersionUID = -2764911804288120883L;

	public static String emptyGraphPath = "graphs/empty.graph";

	private Map<mxCell, State> cellsStates;
	private Map<Agent, Color> agentColors; 
	private mxGraph graph;
	private mxGraphComponent graphComponent;
	private boolean canEdit;

	private JButton oneButton;
	private JButton allButton;
	private JButton startStopButton;
	private SpinnerModel delayModel;
	private IterationThread thread;
	private JButton removeButton;
	private JCheckBox explicitDeadlocksCheckBox;
	private JButton colorButton;
	private ComboBoxModel<AgentTypes> agentTypeModel; 
	private ComboBoxModel<AgentStrategy> agentStrategyModel;
	private SpinnerModel lookAheadStatesModel;
	private JCheckBox setPathinSSCheckBox;
	private JCheckBox sinkCheckBox;
	private SpinnerModel probModel;
	private JLabel statusLabel;
	private JLabel pathLabel;

	private Color addedAgentColor = new Color(255, 0, 0);

	private GraphUtils graphUtils = GraphUtils.getInstance();
	private SystemManager systemManager = SystemManager.getInstance();
	private StateSpace stateSpace = StateSpace.getInstance();

	private File dir;
	private String graphPath;

	public GUI() {
		super("Agent simulator");

		canEdit = true;

		systemManager.setGUI(this);
		dir = new File("graphs");
		thread = new IterationThread(this);

		installToolbars();

		loadGraph(emptyGraphPath, false);
		graphPath = emptyGraphPath;

		// graph component
		graphComponent = new mxGraphComponent(graph);
		graphComponent.setToolTips(true);
		getContentPane().add(graphComponent);

		graphComponent.getGraphControl().addMouseListener(new AgentMouseAdapter(this));

		// handler for adding edges
		graphComponent.getConnectionHandler().addListener(mxEvent.CONNECT, new mxIEventListener() {
			@Override
			public void invoke(Object sender, mxEventObject evt) {
				mxCell edge = (mxCell)evt.getProperty("cell");

				if (edge.getSource() == null || edge.getTarget() == null) {
					graph.removeCells(new Object[] { edge });
					return;
				}

				State src = cellsStates.get(edge.getSource());
				State dest = cellsStates.get(edge.getTarget());

				src.addNeighbor(dest);
			}
		});
	}

	public File getDir(){
		return dir;
	}

	public void setDir(File newDir){
		dir = newDir;
	}

	public String getGraphPath() {
		return graphPath;
	}

	public void setGraphPath(String graphPath) {
		this.graphPath = graphPath;

		String name = new File(graphPath).getName();
		pathLabel.setText("[" + name + "]");
	}

	private int getMaxStateID() {
		int maxID = -1;
		for (State state : stateSpace.getStates()) {
			if (state.getID() > maxID) {
				maxID = state.getID();
			}
		}

		return maxID;
	}

	private int getMaxAgentID() {
		int maxID = -1;
		for (Agent agent : systemManager.getAgents()) {
			if (agent.getID() > maxID) {
				maxID = agent.getID();
			}
		}

		return maxID;
	}

	public void addState(MouseEvent e) {
		State state = new State(getMaxStateID() + 1, sinkCheckBox.isSelected(), (Double)probModel.getValue());

		double scale = graph.getView().getScale();

		mxCell cell = (mxCell) graph.insertVertex(
				graph.getDefaultParent(), null, state.getID(),
				e.getX() / scale, e.getY() / scale, GraphUtils.AGENT_WIDTH, GraphUtils.AGENT_HEIGHT);

		if (sinkCheckBox.isSelected()) {
			cell.setStyle("shape=ellipse");
		}

		cellsStates.put(cell, state);

		stateSpace.addState(state);

		System.out.println("Dodano stan: " + state.getID());
	}

	public void setGoalState(MouseEvent e, mxCell goalCell) {
		mxCell currentCell = (mxCell)graph.getSelectionCell();
		State currentState = cellsStates.get(currentCell);

		if (currentState == null) {
			return;
		}

		Agent agent = currentState.getAgent();

		if (agent == null) {
			return;
		}

		State goalState = cellsStates.get(goalCell);

		if (stateSpace.getGoalStates().containsValue(goalState)) {
			showErrorMsg("Stan " + goalState.getID() + " jest juz stanem docelowym innego agenta");
			return;
		}

		System.out.println("Agent " + currentState.getID() + " idzie do " + goalState.getID());

		stateSpace.addGoalState(agent, goalState);
		agent.setGoalState(goalState);
	}

	public void addAgent(MouseEvent e, mxCell cell) {
		State state = cellsStates.get(cell);

		if (state.getAgent() != null || state.isSink()) {
			return;
		}

		AgentTypes agentType = (AgentTypes) agentTypeModel.getSelectedItem();
		AgentStrategy agentStrategy = (AgentStrategy) agentStrategyModel.getSelectedItem();

		Agent agent = null;
		try {
			agent = (Agent) agentType.getAgentClass().newInstance();
		} catch (InstantiationException e1) {
			System.err.println("Nie mozna utworzyc agenta o podanym typie");
			return;
		} catch (IllegalAccessException e1) {
			System.err.println("Nie mozna utworzyc agenta o podanym typie");
			return;
		}

		agent.setID(getMaxAgentID() + 1);
		agent.setCurrentState(state);
		agent.setStartState(state);
		agent.setGoalState(state);
		agent.setStrategy(agentStrategy);
		agent.setLookAheadStates((Integer)lookAheadStatesModel.getValue());
		agent.setSetPathInStateSpace(setPathinSSCheckBox.isSelected());

		agentColors.put(agent, addedAgentColor);

		stateSpace.addGoalState(agent, state);
		state.setAgent(agent);

		systemManager.addAgent(agent);
		if (agent instanceof ControlledAgent) {
			CentralPlanner.getInstance().addAgent(agent);
		}

		System.out.println("Dodalem " + agent.getID() + " o typie "
				+ agent.getClass().getSimpleName() + " do stanu "
				+ state.getID());
	}

	public void loadEmptyGraph() {
		loadGraph(emptyGraphPath, true);
	}

	public void loadGraph(String filename, boolean reload) {
		cellsStates = new HashMap<mxCell, State>();
		agentColors = new HashMap<Agent, Color>();

		if (reload) {
			stateSpace.loadGraph(this, filename);
			systemManager.loadAgents(filename);
		}

		boolean explicitDeadlocks = explicitDeadlocksCheckBox.isSelected();
		CentralPlanner.getInstance().reset(explicitDeadlocks);

		graph = new AgentGraph(cellsStates);

		if (reload) {
			graphComponent.setGraph(graph);
		}

		graphUtils.drawInitialConfiguration(this);

		setCanEdit(true);

		setRemoveButtonEnabled(false);
		printConfiguration();
	}

	private void installToolbars() {
		// top panel
		JToolBar topToolbar = new JToolBar("topToolbar", JToolBar.HORIZONTAL);

		oneButton = new JButton(new IterationAction("for one", IterationAction.FOR_ONE, this));
		topToolbar.add(oneButton);
		allButton = new JButton(new IterationAction("for all", IterationAction.FOR_ALL, this));
		topToolbar.add(allButton);

		topToolbar.add(new JToolBar.Separator());

		topToolbar.add(new JLabel("Delay: "));
		delayModel = new SpinnerNumberModel(500, 1, 5000, 100);
		JSpinner delaySpinner = new JSpinner(delayModel);
		delaySpinner.setMaximumSize(new Dimension(75, 25));
		topToolbar.add(delaySpinner);

		startStopButton = new JButton (new IterationThreadAction("start", thread, this));
		topToolbar.add(startStopButton);
		topToolbar.add(new JToolBar.Separator());

		topToolbar.add(new JButton(new NewAction("new", this, thread)));
		topToolbar.add(new JButton(new ResetAction("reset", this, thread)));
		topToolbar.add(new JButton(new LoadAction("load", this, thread)));
		topToolbar.add(new JButton(new SaveAction("save", this)));

		topToolbar.add(new JToolBar.Separator());

		JButton generateButton = new JButton(new ShowGenerateDialogAction("generate", this));
		topToolbar.add(generateButton); 

		topToolbar.add(new JToolBar.Separator());

		topToolbar.add(new JButton(new ZoomAction(ZoomAction.ZOOM_IN, this)));
		topToolbar.add(new JButton(new ZoomAction("100%", this)));
		topToolbar.add(new JButton(new ZoomAction(ZoomAction.ZOOM_OUT, this)));
		topToolbar.add(new JButton(new FitAction("fit", this)));

		topToolbar.add(new JToolBar.Separator());

		removeButton = new JButton(new RemoveAction("remove", this));
		topToolbar.add(removeButton);
		removeButton.setEnabled(false);
		
		topToolbar.add(new JToolBar.Separator());
		
		explicitDeadlocksCheckBox = new JCheckBox("deadlocks checking");
		topToolbar.add(explicitDeadlocksCheckBox);
		
		topToolbar.setVisible(true);
		getContentPane().add(topToolbar, BorderLayout.NORTH);

		// left panel
		JToolBar leftToolbar = new JToolBar("leftToolbar", JToolBar.VERTICAL);
		JPanel leftPanel = new JPanel();
		GridLayout layout = new GridLayout(0, 2);
		leftPanel.setLayout(layout);
		leftPanel.setVisible(true);
		leftPanel.setSize(100, 30);
		leftPanel.setMaximumSize(new Dimension(GraphUtils.TOOLBAR_WIDTH, 250));

		leftToolbar.add(leftPanel);
		leftPanel.add(new JLabel("Agent"));
		leftPanel.add(new JToolBar.Separator());

		leftPanel.add(new JLabel("Planning"));
		agentTypeModel = new DefaultComboBoxModel<AgentTypes>(AgentTypes.values());
		JComboBox<AgentTypes> agentTypeCombo = new JComboBox<AgentTypes>(agentTypeModel);
		leftPanel.add(agentTypeCombo);

		leftPanel.add(new JLabel("Strategy"));
		agentStrategyModel = new DefaultComboBoxModel<AgentStrategy>(AgentStrategy.values());
		JComboBox<AgentStrategy> agentStrategyCombo = new JComboBox<AgentStrategy>(agentStrategyModel);
		leftPanel.add(agentStrategyCombo);

		leftPanel.add(new JLabel("Look ahead"));
		lookAheadStatesModel = new SpinnerNumberModel(1, 0, 100, 1);
		JSpinner spinner = new JSpinner(lookAheadStatesModel);
		leftPanel.add(spinner);

		leftPanel.add(new JLabel("Color"));
		colorButton = new JButton(new GetColorAction("     ", this));
		colorButton.setBackground(addedAgentColor);
		leftPanel.add(colorButton);

		leftPanel.add(new JLabel("Set path in SS"));
		setPathinSSCheckBox = new JCheckBox("");
		leftPanel.add(setPathinSSCheckBox);

		leftPanel.add(new JToolBar.Separator());
		leftPanel.add(new JToolBar.Separator());
		leftPanel.add(new JLabel("State"));
		leftPanel.add(new JToolBar.Separator());

		leftPanel.add(new JLabel("Sink"));
		sinkCheckBox = new JCheckBox("");
		leftPanel.add(sinkCheckBox);

		leftPanel.add(new JLabel("Entering prob."));
		probModel = new SpinnerNumberModel(1., 0.1, 1., 0.1);
		JSpinner probSpinner = new JSpinner(probModel);
		probSpinner.setMaximumSize(new Dimension(100, 25));
		leftPanel.add(probSpinner);

		getContentPane().add(leftToolbar, BorderLayout.WEST);

		// bottom panel
		JPanel statusPanel = new JPanel();
		statusPanel.setBorder(new BevelBorder(BevelBorder.LOWERED));
		add(statusPanel, BorderLayout.SOUTH);
		statusPanel.setPreferredSize(new Dimension(getWidth(), 25));
		statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.X_AXIS));

		statusLabel = new JLabel("Editing");
		statusLabel.setHorizontalAlignment(SwingConstants.LEFT);
		statusPanel.add(statusLabel);

		statusPanel.add(new JToolBar.Separator());

		pathLabel = new JLabel(emptyGraphPath);
		pathLabel.setHorizontalAlignment(SwingConstants.RIGHT);
		statusPanel.add(pathLabel);
	}

	private void printConfiguration() {
		for (State state : stateSpace.getStates()) {
			System.out.println(state.getID() + ":");
			for (State neighbor : state.getNeighbors()) {
				System.out.println("\t-> " + neighbor.getID());
			}
		}
		for (Agent agent : systemManager.getAgents()) {
			System.out.println(agent.getID() + ": "
					+ agent.getCurrentState().getID() + " -> "
					+ agent.getGoalState().getID()
					+ " [" + agent.getClass().getSimpleName() + "]");
		}
	}

	public void highlightPath(mxCell cell) {
		if (cell != null && cell.isVertex()) {
			int stateID = cellsStates.get(cell).getID();

			// if someone clicked on edge
			if (stateID < 0) {
				graphUtils.highlightAll(this);
				return;
			}

			for (State state : stateSpace.getStates()) {
				if (state.getID() == stateID) {
					Agent agent = state.getAgent();

					if (agent == null) {
						graphUtils.highlightAll(this);
					}
					else {
						graphUtils.highlightPath(this, agent);
					}
				}
			}
		}
		else {
			graphUtils.highlightAll(this);
		}

		graph.refresh();
	}

	public List<State> getStates() {
		return stateSpace.getStates();
	}

	public List<Agent> getAgents() {
		return systemManager.getAgents();
	}

	public Map<mxCell, State> getCellsStates() {
		return cellsStates;
	}

	public mxCell getCellForState(State state) {
		for (mxCell cell : cellsStates.keySet()) {
			if (cellsStates.get(cell) == state) {
				return cell;
			}
		}

		return null;
	}

	public mxGraph getGraph() {
		return graph;
	}

	public mxGraphComponent getGraphComponent() {
		return graphComponent;
	}

	public int getDelay() {
		return (Integer)delayModel.getValue();
	}

	public void setAddedAgentColor(Color addedAgentColor) {
		this.addedAgentColor = addedAgentColor;

		colorButton.setBackground(addedAgentColor);
	}

	public Color getAddedAgentColor() {
		return addedAgentColor;
	}

	public Color getAgentColor(Agent agent) {
		return agentColors.get(agent);
	}

	public Map<Agent, Color> getAgentColors() {
		return agentColors;
	}

	public void setAgentColor(Agent agent, Color color) {
		agentColors.put(agent, color);
	}

	public void setRemoveButtonEnabled(boolean enabled) {
		removeButton.setEnabled(enabled);
	}

	public boolean getCanEdit() {
		return canEdit;
	}

	public void setCanEdit(boolean canEdit) {
		this.canEdit = canEdit;

		if (canEdit) {
			statusLabel.setText("Edycja");
		}
		else {
			statusLabel.setText("Symulacja");
			setRemoveButtonEnabled(false);
		}
	}

	// called when iteration thread is starting and disables some buttons
	public void startIterationThread() {
		oneButton.setEnabled(false);
		allButton.setEnabled(false);

		setCanEdit(false);

		startStopButton.setText("stop");
	}

	// called when iteration thread is starting and disables some buttons
	public void stopIterationThread() {
		oneButton.setEnabled(true);
		allButton.setEnabled(true);
		startStopButton.setText("start");

		thread.requestStop();
	}

	public void showErrorMsg(String msg){
		JOptionPane.showMessageDialog(new JFrame(), msg, "Dialog", JOptionPane.ERROR_MESSAGE);
	}

	public void showInformationMsg(String msg){
		JOptionPane.showMessageDialog(new JFrame(), msg, "Dialog", JOptionPane.INFORMATION_MESSAGE);
	}

	public static void main(String[] args) {
		GUI frame = new GUI();
		int width = GraphUtils.GRAPH_WIDTH + 5 * GraphUtils.AGENT_WIDTH;
		int height = GraphUtils.GRAPH_HEIGHT + 7 * GraphUtils.AGENT_HEIGHT;

		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setSize(width, height);
		frame.setVisible(true);
	}
}